Organizing Android Projects into Packages and Modules
Course: Android App Development
Topic: Organizing Android Projects into Packages and Modules
Course Link: Android App Development Course
Demo Registration: Register for a Free Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction
Organizing an Android project into packages and modules is an important software development practice that helps developers build applications that are readable, maintainable, reusable, and scalable. As an application grows, it may contain multiple screens, ViewModels, repositories, API clients, database components, utility classes, resources, and automated tests. Placing all these components in a single package or file makes the code difficult to understand and maintain.
Packages organize related classes and interfaces within a source-code namespace, while modules divide a project into larger, independently configured units of functionality. A well-organized project makes it easier to locate code, control dependencies, write tests, collaborate with other developers, and introduce new features.
Android Studio and Gradle support both package-based organization and multi-module project structures. The appropriate approach depends on application size, feature complexity, reuse requirements, and team workflow.
2. Learning Objectives
- Understand the difference between an Android project, module, package, class, and resource directory.
- Learn how to organize Kotlin and Java files into meaningful packages.
- Understand common Android architecture layers and their responsibilities.
- Learn the purpose of app modules, library modules, and feature modules.
- Explore a practical Android project directory structure.
- Understand Gradle module configuration and module dependencies.
- Apply package and module organization to a sample application.
- Identify common project-structure mistakes and learn how to avoid them.
3. Understanding Android Project Organization
3.1 Android Project
An Android project is the complete workspace used to develop an application. It contains one or more modules, source files, resources, build configuration files, tests, and other development assets.
For example, an online shopping application might contain a main app module, a product feature module, a cart feature module, and a shared networking library module.
3.2 Module
A module is a separately configured unit within an Android project. It has its own source sets, build configuration, and dependencies. Modules allow developers to separate functionality into reusable or independently maintained parts.
Common module types include:
- Application module: Produces an installable Android application, usually as an APK or Android App Bundle.
- Android library module: Contains reusable Android code, resources, or components that can be included by other modules.
- Kotlin or Java library module: Contains reusable code that does not necessarily require Android framework APIs.
- Feature module: Groups a particular application feature, such as authentication, products, or profile management.
3.3 Package
A package is a namespace used to group related Kotlin or Java declarations. Packages help developers organize classes according to their responsibilities and reduce naming conflicts.
For example, a shopping application could contain these packages:
com.example.shop.ui
com.example.shop.data
com.example.shop.domain
com.example.shop.network
com.example.shop.database
Packages organize code within a module. They do not automatically create separate Gradle modules or isolate build dependencies.
3.4 Class and Resource
A class defines a type that contains data, behavior, or both. Resources are non-code assets such as images, strings, colors, layouts, and themes. Classes are commonly placed under source directories, while Android resources are placed under the module's src/main/res directory.
4. Difference Between Projects, Modules, Packages, and Classes
| Component |
Purpose |
Example |
| Project |
Contains the complete development workspace. |
ShoppingApp |
| Module |
Separates functionality and build configuration. |
:app, :feature:products |
| Package |
Groups related source-code declarations. |
com.example.shop.data |
| Class |
Defines a type or component. |
ProductRepository |
| Resource directory |
Stores images, strings, layouts, and other resources. |
app/src/main/res |
| Gradle build file |
Defines module plugins, dependencies, and build settings. |
app/build.gradle.kts |
5. Understanding the Default Android Project Structure
When a new Android application is created in Android Studio, the project normally includes an application module and Gradle configuration files. The Android view in the IDE simplifies the display of files, while the Project view shows the underlying directory structure.
A simplified project structure may look like this:
ShoppingApp/
├── app/
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/example/shop/
│ │ │ ├── MainActivity.kt
│ │ │ ├── ui/
│ │ │ ├── data/
│ │ │ └── domain/
│ │ └── res/
│ │ ├── drawable/
│ │ ├── mipmap/
│ │ ├── values/
│ │ └── xml/
│ ├── test/
│ └── androidTest/
├── gradle/
├── build.gradle.kts
├── settings.gradle.kts
└── gradle.properties
This is an illustrative structure. Exact folders vary depending on the project template, programming language, build configuration, and libraries being used.
5.1 The app Directory
The app directory commonly contains the application module. It includes source code, resources, manifest declarations, tests, and the module's Gradle build file.
5.2 The src/main Directory
The src/main directory contains the production application code and resources. Its main elements include:
AndroidManifest.xml — declares application components and relevant application configuration.
java/ — often contains both Kotlin and Java source files despite the directory name.
res/ — stores Android resources such as layouts, images, strings, colors, and themes.
assets/ — stores files that the application needs to read in their original form.
5.3 The test and androidTest Directories
src/test/ contains local tests that commonly run on the JVM, such as repository or business-logic unit tests.
src/androidTest/ contains instrumented tests that run on an Android device or emulator, such as UI tests or tests requiring Android framework behavior.
5.4 Gradle Configuration Files
Gradle configuration defines how the project is built and which libraries it uses.
settings.gradle.kts identifies the root project and includes its modules.
- The root
build.gradle.kts configures shared build logic and plugin declarations.
- The module-level
build.gradle.kts configures the module's plugins, SDK settings, and dependencies.
gradle.properties stores Gradle properties used by the build.
6. Organizing Source Code into Packages
Packages should reflect the responsibility of the code they contain. A package structure makes it easier to find a class and understand which part of the application it belongs to.
6.1 Use a Consistent Base Package
Start with a reverse-domain-style package name, such as com.example.shoppingapp. In a real published application, use a package or application ID that is appropriately unique for the app.
Example:
com.example.shoppingapp
com.example.shoppingapp.ui
com.example.shoppingapp.data
com.example.shoppingapp.domain
6.2 Group Classes by Responsibility
A useful starting point is to group code by architectural layer or feature. Avoid placing unrelated classes together merely because they use the same programming language.
| Package |
Typical contents |
ui |
Screen composables, fragments, activities, and presentation components. |
ui.viewmodel |
ViewModels and UI state holders when this structure suits the project. |
domain |
Business models, use cases, and business rules. |
data |
Repository implementations and data-management code. |
data.remote |
Network services, API response models, and remote data sources. |
data.local |
Database entities, DAO interfaces, and local data sources. |
navigation |
Navigation routes and navigation-related setup. |
di |
Dependency injection configuration when a dedicated package is useful. |
util |
Small, genuinely reusable helper functions or classes. |
6.3 Avoid Oversized Utility Packages
A package named utils can be useful for a small number of general helpers, but it should not become a place for every class that does not fit elsewhere. Prefer descriptive names that reveal a component's purpose.
For example, instead of placing a price calculation, image loader, date formatter, and API authentication helper in one generic package, organize them according to their actual responsibilities.
6.4 Follow Kotlin and Java Naming Conventions
- Package names are normally lowercase, such as
com.example.shop.data.
- Class and interface names normally use PascalCase, such as
ProductRepository.
- Function and property names normally use camelCase, such as
getProducts().
- Use meaningful names that explain the component's responsibility.
- Avoid inconsistent abbreviations and unexplained acronyms.
7. Organizing by Architecture Layers
Android architecture commonly separates the UI layer and data layer, with an optional domain layer when business logic needs additional organization or reuse. Packages can represent these layers even when the project remains a single Gradle module.
7.1 UI Layer
The UI layer displays application information and handles user interaction. It may contain activities, fragments, Jetpack Compose functions, UI state models, and ViewModels.
Example package:
com.example.shoppingapp.ui.products
Typical responsibilities include displaying products, responding to button clicks, collecting UI state, and rendering loading or error states. The UI should not become responsible for all networking and database operations.
7.2 Domain Layer
The domain layer is optional. It can contain business models and use cases when business rules are complex or reused by multiple ViewModels.
Example package:
com.example.shoppingapp.domain
com.example.shoppingapp.domain.model
com.example.shoppingapp.domain.usecase
A use case should normally represent one clear action, such as calculating a cart total or retrieving a list of available products.
7.3 Data Layer
The data layer manages application data. Repositories provide a consistent interface for accessing data and may coordinate remote APIs, local databases, caches, or other data sources.
Example package:
com.example.shoppingapp.data
com.example.shoppingapp.data.repository
com.example.shoppingapp.data.remote
com.example.shoppingapp.data.local
Repositories help prevent screens and ViewModels from depending directly on implementation details of a network client or database.
8. Feature-Based Package Organization
For applications with several screens or business features, grouping code by feature can make development easier. In this approach, each feature has a recognizable place for its UI, state holders, and related components.
Example:
com.example.shoppingapp/
├── app/
├── feature/
│ ├── auth/
│ │ ├── LoginScreen.kt
│ │ ├── LoginViewModel.kt
│ │ └── AuthUiState.kt
│ ├── products/
│ │ ├── ProductListScreen.kt
│ │ ├── ProductDetailsScreen.kt
│ │ └── ProductsViewModel.kt
│ ├── cart/
│ │ ├── CartScreen.kt
│ │ └── CartViewModel.kt
│ └── profile/
│ ├── ProfileScreen.kt
│ └── ProfileViewModel.kt
├── data/
├── domain/
└── core/
The example illustrates a logical grouping. In a real project, directories under feature could be packages within one module or separate Gradle modules, depending on the project's needs.
8.1 Layer-Based Versus Feature-Based Organization
| Approach |
Structure |
Useful when |
| Layer-based |
All UI classes are grouped together, all repositories together, and all data sources together. |
The application is small or the architecture is straightforward. |
| Feature-based |
Classes related to each feature are grouped together. |
The application has many features or several developers work on separate areas. |
| Hybrid |
Features are grouped together while shared data and core components remain separate. |
The application needs a balance between feature ownership and code reuse. |
No single package structure is ideal for every project. Choose a structure that reflects the application's responsibilities and expected growth, and keep it consistent across the codebase.
9. Understanding Android Modules
Packages organize source code, but modules provide a stronger boundary. Each module can have its own dependencies, build settings, source sets, and tests. Modules are useful when parts of the application need clearer ownership, reuse, isolation, or independent build configuration.
9.1 Application Module
The application module usually contains the application entry point, app-level configuration, and the composition of features. It commonly applies the Android application plugin.
Example module name: :app
9.2 Android Library Module
An Android library module can contain reusable Android components, resources, or functionality shared by multiple modules.
Example module names:
:core:ui — shared UI components and design-system elements.
:core:network — networking configuration and shared API infrastructure.
:core:database — shared database configuration and data-access components.
9.3 Kotlin or Java Library Module
A plain Kotlin or Java library is appropriate for reusable code that does not require Android-specific APIs. For example, a pure Kotlin module could contain domain models or business rules that can be tested independently of the Android framework.
9.4 Feature Module
A feature module groups a specific capability, such as login, product browsing, checkout, or user profiles. It can help a team define clear ownership and reduce the number of unrelated dependencies in the main application module.
Example module names:
:feature:auth
:feature:products
:feature:cart
:feature:profile
Feature modules should expose clear interfaces and avoid depending on one another unnecessarily. Shared functionality should be placed in suitable shared modules rather than copied between features.
10. Example Multi-Module Project Structure
For a medium-sized shopping application, a possible multi-module structure is:
ShoppingApp/
├── app/
│ └── build.gradle.kts
├── core/
│ ├── common/
│ │ └── build.gradle.kts
│ ├── ui/
│ │ └── build.gradle.kts
│ ├── network/
│ │ └── build.gradle.kts
│ └── database/
│ └── build.gradle.kts
├── domain/
│ └── build.gradle.kts
├── data/
│ └── build.gradle.kts
├── feature/
│ ├── auth/
│ │ └── build.gradle.kts
│ ├── products/
│ │ └── build.gradle.kts
│ ├── cart/
│ │ └── build.gradle.kts
│ └── profile/
│ └── build.gradle.kts
├── settings.gradle.kts
└── build.gradle.kts
This structure is an example, not a required template. A smaller application may work perfectly well with a single :app module and well-designed packages. Add modules only when they create a meaningful boundary or provide a concrete benefit.
11. Configuring Modules in Gradle
Gradle needs to know which modules belong to the project. In a Kotlin DSL project, the root settings file commonly includes module paths.
11.1 Include Modules in settings.gradle.kts
For example, a project with an app module and two feature modules could use:
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(
RepositoriesMode.FAIL_ON_PROJECT_REPOS
)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "ShoppingApp"
include(":app")
include(":feature:products")
include(":feature:cart")
include(":core:common")
Each included path must correspond to a valid module directory and build configuration. Projects using convention plugins, version catalogs, or centralized build logic may have additional configuration.
11.2 Configure an Application Module
A simplified application module build file might look like this:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.shoppingapp"
compileSdk = 35
defaultConfig {
applicationId = "com.example.shoppingapp"
minSdk = 23
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
}
dependencies {
implementation(project(":feature:products"))
implementation(project(":feature:cart"))
implementation(project(":core:common"))
}
Important: The SDK levels and plugin configuration above are illustrative. Choose SDK levels and plugin versions that match the current requirements of your project. Some projects use a version catalog or convention plugins instead of declaring plugin versions in every build file.
11.3 Configure a Library Module
A reusable Android library module uses the Android library plugin instead of the application plugin.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.core.common"
compileSdk = 35
defaultConfig {
minSdk = 23
}
}
A library module does not use an application ID to identify an installable app. Its namespace is used for generated code and resource identifiers. A plain Kotlin/JVM library would use a Kotlin/JVM plugin configuration instead.
11.4 Add Module Dependencies
A module can depend on another module by using Gradle's project dependency notation.
dependencies {
implementation(project(":core:common"))
}
The implementation configuration is suitable for dependencies needed internally by the module. Use api only when a dependency's public types intentionally need to be exposed to consumers. Excessive use of api can increase coupling and cause implementation details to leak across module boundaries.
12. Understanding Dependency Direction
A healthy architecture keeps dependencies intentional and predictable. For example, a UI feature can depend on a domain interface, while a data implementation supplies the concrete repository. The domain layer should not need to depend on Android screens.
A simplified dependency direction might be:
app
├── feature:products
├── feature:cart
└── data
├── domain
├── core:network
└── core:database
feature:products
└── domain
feature:cart
└── domain
This diagram is only one possible arrangement. The exact dependency graph depends on the architecture and dependency-injection strategy. The key goal is to prevent circular dependencies and avoid making every module depend on every other module.
12.1 Avoid Circular Dependencies
A circular dependency occurs when module A depends on module B and module B also depends on module A, either directly or through a chain of dependencies. Such cycles make the architecture harder to understand and can prevent Gradle from resolving the project structure as intended.
To resolve a cycle, extract a shared interface or model into a suitable lower-level module, or redesign the responsibility boundary so that dependencies flow in one direction.
13. Creating Packages in Android Studio
Follow these steps to create a package in Android Studio:
- Open the Android project in Android Studio.
- In the Project tool window, locate the relevant app or library module.
- Navigate to
src/main/java or the source directory used by your module.
- Locate the existing base package.
- Right-click the package and choose New > Package.
- Enter the package name, such as
com.example.shoppingapp.data.repository.
- Create a Kotlin or Java class inside the new package.
- Check the package declaration at the top of the source file.
- Run the project or its relevant tests to confirm that imports and references remain valid.
When renaming or moving existing packages, prefer Android Studio's refactoring tools rather than moving files manually. Refactoring helps update package declarations and references, though the result should still be reviewed and tested.
14. Creating a Module in Android Studio
To create a new module, use the module creation workflow supported by your Android Studio version:
- Open the project in Android Studio.
- Open the File menu and select the New Module option, if available in your version.
- Choose an appropriate module type, such as an Android library or application module.
- Enter the module name and required configuration.
- Complete the wizard and allow Gradle to synchronize.
- Check that the module is included in the project settings.
- Add project dependencies where required.
- Build and test the relevant modules.
Module creation options can vary by Android Studio version and installed plugins. If the wizard does not offer the desired module type, consult the current Android Studio documentation and configure the module's Gradle files appropriately.
15. Practical Example: Organizing a Product Feature
Consider an application that displays a list of products. A poorly organized implementation might place the screen, API call, JSON parsing, and price calculations in a single activity. This makes the screen difficult to test and reuse.
A better structure separates presentation, business rules, and data access.
15.1 Product Model
package com.example.shoppingapp.domain.model
data class Product(
val id: Int,
val name: String,
val price: Double
)
The product model represents the information the application needs to work with. In a larger project, the domain model may differ from the API response model or database entity.
15.2 Repository Interface
package com.example.shoppingapp.domain.repository
import com.example.shoppingapp.domain.model.Product
interface ProductRepository {
suspend fun getProducts(): List<Product>
}
The interface describes the operation needed by the application without requiring the caller to know whether the data comes from a remote service, local database, or cache.
15.3 Repository Implementation
package com.example.shoppingapp.data.repository
import com.example.shoppingapp.domain.model.Product
import com.example.shoppingapp.domain.repository.ProductRepository
class ProductRepositoryImpl : ProductRepository {
override suspend fun getProducts(): List<Product> {
// Replace with a real API or database operation.
return listOf(
Product(1, "Keyboard", 1499.0),
Product(2, "Mouse", 799.0)
)
}
}
This example uses sample data to demonstrate package boundaries. A production implementation would normally delegate to an appropriate data source and handle loading failures, mapping, and caching as needed.
15.4 Use Case
package com.example.shoppingapp.domain.usecase
import com.example.shoppingapp.domain.model.Product
import com.example.shoppingapp.domain.repository.ProductRepository
class GetProductsUseCase(
private val repository: ProductRepository
) {
suspend operator fun invoke(): List<Product> {
return repository.getProducts()
}
}
The use case provides a clear entry point for retrieving products. For a simple application, a separate use case may be unnecessary; add one when it makes business logic clearer or more reusable.
15.5 ViewModel
package com.example.shoppingapp.ui.products
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.shoppingapp.domain.usecase.GetProductsUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class ProductsViewModel(
private val getProducts: GetProductsUseCase
) : ViewModel() {
private val _products =
MutableStateFlow(emptyList<com.example.shoppingapp.domain.model.Product>())
val products: StateFlow<List<com.example.shoppingapp.domain.model.Product>> =
_products.asStateFlow()
fun loadProducts() {
viewModelScope.launch {
_products.value = getProducts()
}
}
}
This example is simplified to demonstrate package organization. A production ViewModel should usually expose explicit loading, success, and error states and use a dependency-injection or factory mechanism to supply its dependencies. Importing the Product model can also make the type declarations more readable.
16. Testing an Organized Project
A clear project structure makes it easier to test each component at the appropriate level.
- Domain tests: Test use cases and business rules without needing an Android device.
- Repository tests: Test data mapping, caching rules, and behavior when data sources succeed or fail.
- ViewModel tests: Verify state updates, loading behavior, and error handling.
- UI tests: Check whether the application displays the correct information and responds to user interaction.
- Module-level validation: Build and test modules to detect dependency or compilation problems early.
Keep test code in the appropriate source set and use dependency injection or constructor parameters to replace real data sources with test doubles where necessary.
17. Benefits of Organizing Packages and Modules
| Benefit |
Explanation |
| Maintainability |
Developers can locate and modify code more easily. |
| Readability |
Package names and module boundaries communicate responsibility. |
| Reusability |
Shared libraries can be reused by multiple features or applications. |
| Testability |
Components with clear dependencies are easier to test in isolation. |
| Encapsulation |
Modules can limit which implementation details are exposed to consumers. |
| Team collaboration |
Feature boundaries can help teams work in parallel with fewer conflicts. |
| Scalability |
New functionality can be added without turning the entire codebase into one tightly coupled unit. |
| Build management |
Modularity can support incremental builds and parallel work, depending on the project's dependency graph and Gradle configuration. |
18. Common Mistakes and How to Avoid Them
18.1 Putting Everything in One Package
Problem: Activities, repositories, models, API clients, and helper functions are mixed together.
Solution: Introduce packages based on clear responsibilities and keep related classes together.
18.2 Creating Too Many Packages
Problem: Every class has its own package, producing unnecessary navigation and complexity.
Solution: Create packages around meaningful concepts rather than individual files.
18.3 Creating Too Many Modules Too Early
Problem: Small, closely related components are separated into modules that add configuration and dependency overhead without providing meaningful benefits.
Solution: Begin with a sensible package structure and introduce modules when independent build configuration, reuse, ownership, or dependency boundaries justify them.
18.4 Creating Modules That Are Too Large
Problem: A module becomes a large, tightly coupled unit containing many unrelated features.
Solution: Review feature boundaries and consider extracting stable, cohesive areas into separate modules when the benefits outweigh the overhead.
18.5 Exposing Too Many Internal Classes
Problem: Other modules become dependent on implementation details, making future changes difficult.
Solution: Expose only the public interfaces needed by consumers. Use Kotlin visibility modifiers such as internal and private where appropriate.
18.6 Creating Circular Dependencies
Problem: Two modules depend on each other, or a feature relies on another feature in a way that creates a dependency cycle.
Solution: Redesign the dependency direction, introduce suitable interfaces, or move shared contracts into a lower-level module.
18.7 Mixing UI and Data Responsibilities
Problem: Activities or composables directly manage networking, database queries, and business rules.
Solution: Keep UI responsibilities in the UI layer and move data operations into appropriate repositories and data sources.
19. Best Practices for Android Project Organization
- Use a consistent package naming convention across the project.
- Choose a clear separation of concerns for UI, domain, and data responsibilities.
- Group related code by feature when it improves navigation and ownership.
- Keep modules cohesive and give each one a clear purpose.
- Minimize dependencies between modules and avoid circular dependency chains.
- Keep public APIs small and hide internal implementation details.
- Use Gradle project dependencies deliberately; do not expose dependencies through
api without a reason.
- Place reusable code in a shared module only when reuse or a clear architectural boundary justifies it.
- Keep unit tests and instrumented tests in their appropriate source sets.
- Use Android Studio refactoring tools when moving or renaming packages and classes.
- Document non-obvious module responsibilities and dependency rules.
- Review the project structure as the application grows instead of following a rigid structure that no longer fits.
- Use official Android architecture and modularization guidance as a reference when making design decisions.
20. Troubleshooting Package and Module Problems
| Problem |
Possible cause |
Recommended solution |
| Unresolved reference |
Incorrect import, package declaration, missing source file, or missing dependency. |
Check the package declaration, imports, and module dependency configuration. |
| Module not found |
The module is not included in the project settings or its directory configuration is incorrect. |
Review settings.gradle.kts and verify the module path. |
| Gradle sync fails |
Invalid build syntax, unavailable plugin, repository configuration, or incompatible dependency versions. |
Read the first meaningful error in the Gradle sync output and correct the underlying configuration. |
| Duplicate class error |
Multiple dependencies contain the same class or code is duplicated across modules. |
Inspect dependency declarations and remove unintended duplicate implementations. |
| Resource not found |
The resource is missing, incorrectly named, or referenced from an inappropriate module. |
Check the resource directory, resource name, and dependency on the module that owns it. |
| Manifest or namespace issue |
A module's manifest or namespace configuration is incomplete or incorrect. |
Review the module's Android configuration and manifest declarations. |
| Tests cannot access a class |
The class has restricted visibility or is in the wrong source set. |
Check visibility, test source sets, and whether the test belongs in the same module. |
| Circular module dependency |
Modules depend on each other directly or indirectly. |
Extract a shared contract or redesign the module boundaries and dependency direction. |
21. Practical Exercise
Exercise: Organize a simple task-management application using packages and, if appropriate, multiple modules.
The application should include the following functionality:
- Display a list of tasks.
- Add a new task.
- Mark a task as completed.
- Store tasks locally.
- Display loading or error information when necessary.
Suggested Package Structure
com.example.taskmanager/
├── ui/
│ ├── tasks/
│ │ ├── TaskListScreen.kt
│ │ ├── TaskViewModel.kt
│ │ └── TaskUiState.kt
│ └── navigation/
├── domain/
│ ├── model/
│ ├── repository/
│ └── usecase/
├── data/
│ ├── repository/
│ └── local/
└── di/
Exercise Tasks
- Create the base package for the application.
- Create packages for UI, domain, and data responsibilities.
- Define a task model and a repository interface.
- Implement a repository using a local data source.
- Create a ViewModel that exposes task state to the UI.
- Write unit tests for task-related business logic.
- Build the application and verify that the package structure compiles correctly.
- Decide whether any component would benefit from being extracted into a separate Gradle module, and explain why.
22. Mini Project: Modular Shopping Application
For a more advanced exercise, design a shopping application with authentication, products, cart, and profile features.
Suggested Modules
:app
:feature:auth
:feature:products
:feature:cart
:feature:profile
:core:ui
:core:network
:core:database
:domain
:data
Project Requirements
- Keep feature-specific screens and state holders within the relevant feature boundary.
- Place reusable UI components in the shared UI module when appropriate.
- Use repository interfaces and implementations to separate business logic from data access.
- Define clear Gradle dependencies and avoid circular module references.
- Write tests for repositories, use cases, and ViewModels.
- Document the purpose of each module and the dependencies it is allowed to use.
Expected outcome: A maintainable project in which each package and module has a clear responsibility and features can be developed and tested with fewer unnecessary dependencies.
23. Interview Questions and Answers
Q1. What is a package in Android development?
Answer: A package is a namespace that groups related Kotlin or Java declarations. It improves code organization and helps avoid naming conflicts.
Q2. What is a module in an Android project?
Answer: A module is a separately configured unit of a project with its own build settings, source sets, and dependencies. Modules can represent the application, reusable libraries, or individual features.
Q3. What is the difference between a package and a module?
Answer: A package organizes source-code declarations within a namespace. A module establishes a larger boundary for source files, resources, build configuration, and dependencies.
Q4. What is the purpose of settings.gradle.kts?
Answer: It defines the root project and identifies which modules are included in the Gradle build. It can also configure plugin and dependency repositories.
Q5. What is the difference between an application module and an Android library module?
Answer: An application module is configured to produce an installable app. An Android library module provides code and possibly resources that other modules can depend on.
Q6. Why should an Android project be organized into packages?
Answer: Packages make classes easier to locate, improve readability, support separation of concerns, and help developers maintain consistent code boundaries.
Q7. What is modularization?
Answer: Modularization is the practice of splitting a codebase into loosely coupled, cohesive modules with clear responsibilities and controlled dependencies.
Q8. What is a feature module?
Answer: A feature module groups code related to a particular application capability, such as authentication, product browsing, or checkout.
Q9. What is the purpose of a core module?
Answer: A core module provides shared functionality, such as networking, common utilities, reusable UI components, or database infrastructure, to other modules.
Q10. What is the difference between implementation and api in Gradle?
Answer: The implementation configuration keeps a dependency internal to a module's implementation for consumers. The api configuration exposes a dependency on the module's public API to consumers and should be used only when that exposure is intentional.
Q11. Why should circular dependencies be avoided?
Answer: Circular dependencies make the architecture harder to maintain and can prevent Gradle from resolving the build. Shared interfaces or redesigned module boundaries can help remove cycles.
Q12. Should every Android application use multiple modules?
Answer: No. Small applications can use a single app module with well-organized packages. Multiple modules are useful when they provide clear benefits such as reuse, encapsulation, team ownership, or independent configuration.
Q13. What is the role of the domain layer?
Answer: The domain layer contains business models and use cases that represent application rules. It is optional and is most useful when business logic needs additional separation or reuse.
Q14. How can Android Studio help reorganize packages?
Answer: Android Studio provides package creation and refactoring tools that can move or rename classes while updating package declarations and references.
Q15. How does a good module structure improve testing?
Answer: Clear module boundaries and limited dependencies make components easier to isolate. Developers can test business logic, repositories, and features without involving the entire application.
24. Quick Revision Checklist
- Understand the distinction between projects, modules, packages, classes, and resources.
- Use consistent package naming and meaningful class names.
- Organize code according to clear responsibilities.
- Separate UI logic from data-access responsibilities.
- Use a domain layer when it provides a real architectural benefit.
- Choose layer-based, feature-based, or hybrid package organization according to project needs.
- Understand application, Android library, and plain Kotlin or Java library modules.
- Know how to include modules in
settings.gradle.kts.
- Use Gradle project dependencies intentionally.
- Avoid circular dependencies and unnecessary public APIs.
- Keep tests in suitable source sets and validate changes with builds and tests.
- Do not introduce extra modules unless their benefits justify the maintenance overhead.
25. Conclusion
Organizing Android projects into packages and modules is a foundational skill for building maintainable applications. Packages group related source-code declarations, while modules provide stronger boundaries for functionality, dependencies, and build configuration. Combining a clear package structure with appropriate architecture layers makes an application easier to understand, test, and extend.
Start with a simple, consistent structure and introduce additional modules when the project's size, feature boundaries, reuse requirements, or team workflow justify them. The goal is not to create the largest possible number of packages or modules, but to establish clear responsibilities and dependencies that support long-term development.
26. Additional Learning Resources